using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Linq;


namespace GuavaData
{
    public class VSplit : Transform
    {
        string t;
        string n;
        List<string> columns;
        List<string> keepfk;

        // Fields for run-time state of the columns
        List<string> inputKeys;
        List<Domain.Domain> inputKeysDomains;
        Dictionary<string, Domain.Domain> inputColumns;
        
        /// <summary>
        /// Vertical split operation for a single table in a schema
        /// </summary>
        /// <param name="table">The table to split</param>
        /// <param name="newTable">The name of the new table for the split-off columns</param>
        /// <param name="columns">The non-id/fk columns to keep in the original table - id columns go into both tables, and fk columns stay only in the original</param>
        public VSplit(string table, string newTable, List<string> columns)
        {
            this.t = table;
            this.n = newTable;
            this.columns = columns;
            this.keepfk = new List<string>();

            // Until the schema is pushed through, do not assign to values in memory
            this.inputKeys = null;
            this.inputKeysDomains = null;
            this.inputColumns = null;
        }

        internal override void Expand(AddColumn ac, Command c)
        {
            // If the table doesn't match, punt
            if (ac.Table != t)
                return;
            
            // If the column already exists, throw error
            if (this.inputColumns.ContainsKey(ac.Column))
                throw new Exception("Column " + ac.Column + " already exists in table " + ac.Table + ".");

            // Add the column to the internal represenation
            this.inputColumns[ac.Column] = ac.Domain;

            // If this new column is not in the list of keep columns, alter its table so that it points to the split table
            if (!(this.columns.Contains(ac.Column)))
                ac.Table = this.n;
        }

        internal override void Expand(AddElement ae, Command c)
        {
            // If the table doesn't match, punt
            if (ae.Table != t)
                return;

            // If the domain contains the element, throw
            Domain.Enumerated e = this.inputColumns[ae.Column] as Domain.Enumerated;
            int i = e.Items.IndexOf(ae.Element);
            if (i >= 0)
                throw new Exception("Domain already contains the element " + ae.Element + ".");

            // If column is one of the keys, propagate to both tables
            AddElement aenew = new AddElement(this.n, ae.Column, ae.Element);
            if (this.inputKeys.Contains(ae.Column))
                c.NotifyChange(c, new Command(aenew));
            else if (!(this.columns.Contains(ae.Column)))
                c.NotifyChange(new Command(aenew));

            // Alter the domain
            Domain.Enumerated enew = new GuavaData.Domain.Enumerated(e.Items);
            enew.Items.Add(ae.Element);
            this.inputColumns[ae.Column] = enew;
        }
        
        internal override void Expand(AddTable at, Command c)
        {
            // If the table doesn't match, punt
            if (at.Table != t)
                return;

            // If the table already exists, throw an error - table exists if the dictionary of columns is present
            if (this.inputColumns != null)
                throw new Exception("Table " + at.Table + " already exists.");

            // Partition into two tables - keep and split
            AddTable atkeep = new AddTable(this.t);
            atkeep.EnforcePrimaryKey = at.EnforcePrimaryKey;
            AddTable atsplit = new AddTable(this.n);
            atsplit.EnforcePrimaryKey = at.EnforcePrimaryKey;
            ForeignKey fk = new ForeignKey(n, t);

            // Loop through the columns in the add table list.
            this.inputColumns = new Dictionary<string,GuavaData.Domain.Domain>();
            this.inputKeys = new List<string>();
            this.inputKeysDomains = new List<GuavaData.Domain.Domain>();
            for (int i = 0; i < at.Columns.Count; i++)
            {
                if (at.Keys.Contains(at.Columns[i]))
                {
                    atkeep.AddColumn(at.Columns[i], at.Domains[i], true);
                    atsplit.AddColumn(at.Columns[i], at.Domains[i], true);
                    this.inputKeys.Add(at.Columns[i]);
                    this.inputKeysDomains.Add(at.Domains[i].Clone);
                    fk.AddColumn(at.Columns[i], at.Columns[i]);
                }
                else if (this.columns.Contains(at.Columns[i]))
                    atkeep.AddColumn(at.Columns[i], at.Domains[i], false);
 
                else
                    atsplit.AddColumn(at.Columns[i], at.Domains[i], false);

                this.inputColumns[at.Columns[i]] = at.Domains[i].Clone;
            }
            
            c.NotifyChange(new Command(atkeep), new Command(atsplit), new Command(fk));
        }

        internal override void Expand(AlterColumnRename acr, Command c)
        {
            // If the table doesn't match, punt
            if (acr.Table != t)
                return;

            // If the column already exists, throw an error
            if (this.inputColumns.ContainsKey(acr.NewName))
                throw new Exception("Column " + acr.NewName + " already exists in table " + acr.Table + ".");

            // If column is one of the keys, propagate to both tables
            AlterColumnRename acrnew = new AlterColumnRename(this.n, acr.OldName, acr.NewName);
            if (this.inputKeys.Contains(acr.OldName))
            {
                c.NotifyChange(c, new Command(acrnew));
                inputKeys.Remove(acr.OldName);
                inputKeys.Add(acr.NewName);
            }
            else if (!(this.columns.Contains(acr.OldName)))
            {
                // If the new column name is in the keep list, however, we must move the column from the split table to the keep table
                if (this.columns.Contains(acr.NewName))
                {
                    // In this case, we throw an error - we don't want unexpected side effects
                    throw new DDLSideEffectException("Possible unforseen side effect encountered in a VSplit operation: " + acr.OldName
                        + " is not in the list of columns to keep in the original table " + acr.Table + ", but " + acr.NewName
                        + " is.  Please update this operator's parameters to complete this operation.");
                }
                else
                {
                    c.NotifyChange(new Command(acrnew));
                }
            }
            else
            {
                // Update internals
                this.columns.Remove(acr.OldName);

                if (!(this.columns.Contains(acr.NewName)))
                    this.columns.Add(acr.NewName);
            }

            this.inputColumns[acr.NewName] = this.inputColumns[acr.OldName];
            this.inputColumns.Remove(acr.OldName);
        }

        internal override void Expand(AlterElementRename aer, Command c)
        {
            // If the table doesn't match, punt
            if (aer.Table != t)
                return;
            
            // If the domain does not contain the element, throw
            Domain.Enumerated e = this.inputColumns[aer.Column] as Domain.Enumerated;
            int i = e.Items.IndexOf(aer.OldName);
            if (i < 0)
                throw new Exception("Domain does not contain the element " + aer.OldName + ".");

            // If column is one of the keys, propagate to both tables
            AlterElementRename aernew = new AlterElementRename(this.n, aer.Column, aer.OldName, aer.NewName);
            if (this.inputKeys.Contains(aer.Column))
                c.NotifyChange(c, new Command(aernew));
            else if (!(this.columns.Contains(aer.Column)))
                c.NotifyChange(new Command(aernew));
            
            // Alter the domain
            Domain.Enumerated enew = new GuavaData.Domain.Enumerated(e.Items);
                enew.Items.RemoveAt(i);
            enew.Items.Insert(i, aer.NewName);
            this.inputColumns[aer.Column] = enew;
        }

        internal override void Expand(AlterTableRename atr, Command c)
        {
            if (atr.OldName == t)
            {
                // Verify that table name conflicts do not occur
                if (atr.NewName == n)
                    throw new DDLSideEffectException("Cannot rename a table to " + n + " because a vertical split operator generates a table with that name.");

                t = atr.NewName;
            }
            else
            {
                // Verify that the name change does not make the operator suddently act on a table that it did not act on before
                if (atr.NewName == t || atr.NewName == n)
                    throw new DDLSideEffectException("Cannot rename a table to " + atr.NewName + " because it conflicts with one of the parameters of a VSplit operator.");
            }
        }

        internal override void Expand(Delete d, Command c)
        {
            // If the table doesn't match, punt
            if (d.Table != t)
                return;

            Delete dkeep = new Delete(t);
            Delete dsplit = new Delete(n);
            for (int i = 0; i < d.Columns.Count; i++)
            {
                dkeep.AddCondition(d.Columns[i], d.Conditions[i]);
                dsplit.AddCondition(d.Columns[i], d.Conditions[i]);
            }

            c.NotifyChange(new Command(dsplit), new Command(dkeep));
        }

        internal override void Expand(DropColumn dc, Command c)
        {
            // If the table doesn't match, punt
            if (dc.Table != t)
                return;

            // If the column does not exist, throw error
            if (!(this.inputColumns.ContainsKey(dc.Column)))
                throw new Exception("Column " + dc.Column + " does not exist in table " + dc.Table + ".");

            // Add the column to the internal represenation
            this.inputColumns.Remove(dc.Column);

            // If this new column is not in the list of keep columns, alter its table so that it points to the split table
            if (!(this.columns.Contains(dc.Column)))
                dc.Table = this.n;
        }

        internal override void Expand(DropElement de, Command c)
        {
            // If the table doesn't match, punt
            if (de.Table != t)
                return;

            // If the domain does not contain the element, throw
            Domain.Enumerated e = this.inputColumns[de.Column] as Domain.Enumerated;
            int i = e.Items.IndexOf(de.Element);
            if (i < 0)
                throw new Exception("Domain does not contain the element " + de.Element + ".");

            // If column is one of the keys, propagate to both tables
            DropElement denew = new DropElement(this.n, de.Column, de.Element);
            if (this.inputKeys.Contains(de.Column))
                c.NotifyChange(c, new Command(denew));
            else if (!(this.columns.Contains(de.Column)))
                c.NotifyChange(new Command(denew));

            // Alter the domain
            Domain.Enumerated enew = new GuavaData.Domain.Enumerated(e.Items);
            enew.Items.RemoveAt(i);
            this.inputColumns[de.Column] = enew;
        }
        
        internal override void Expand(DropTable dt, Command c)
        {
            // Create a drop table command for the child table, and null out the internal memory
            if (dt.Table != t)
                return;

            this.inputColumns.Clear();
            this.inputColumns = null;
            this.inputKeys.Clear();
            this.inputKeys = null;
            this.inputKeysDomains.Clear();
            this.inputKeysDomains = null;

            DropTable dtnew  = new DropTable(this.n);
            c.NotifyChange(c, new Command(dtnew));
        }

        internal override void Expand(ForeignKey fk, Command c)
        {
            if (fk.ChildTable != t)
                return;

            // Determine if the foreign key needs to be adjusted or removed
            bool bkeep = false;
            bool bsplit = false;
            foreach (string col in fk.ChildColumns)
            {
                if (this.inputKeys.Contains(col))
                    continue;
                else if (this.columns.Contains(col))
                    bkeep = true;
                else
                    bsplit = true;
            }

            if (bkeep && bsplit)
                c.NotifyChange();
            else if (bsplit)
                fk.ChildTable = n;
        }
        /*
        internal override void Expand(Insert i, Command c)
        {
            // If the table doesn't match, punt
            if (i.Table != t)
                return;

            // Process the child
            i.Child.Visit(this, c);
            bool isRowConstant = (i.Child is RowConstant);
            RowConstant rc1 = new RowConstant();
            RowConstant rc2 = new RowConstant();

            List<string> keepCols = new List<string>();
            List<string> splitCols = new List<string>();

            // If child is a full query, copy the query and project only the columns that are needed
            List<string> keepProj = new List<string>();
            List<string> splitProj = new List<string>();

            for (int j = 0; j < i.Columns.Count; j++)
            {
                if (inputKeys.Contains(i.Columns[j]))
                {
                    keepCols.Add(i.Columns[j]);
                    splitCols.Add(i.Columns[j]);
                    if (isRowConstant)
                    {
                        rc1.AddValue(i.Child.exposedColumns[j], i.Child.exposedDomains[j], (i.Child as RowConstant).Fields[j]);
                        rc2.AddValue(i.Child.exposedColumns[j], i.Child.exposedDomains[j], (i.Child as RowConstant).Fields[j]);
                    }
                    else
                    {
                        keepProj.Add(i.Child.exposedColumns[j]);
                        splitProj.Add(i.Child.exposedColumns[j]);
                    }
                }
                else if (columns.Contains(i.Columns[j]))
                {
                    keepCols.Add(i.Columns[j]);
                    if (isRowConstant)
                    {
                        rc1.AddValue(i.Child.exposedColumns[j], i.Child.exposedDomains[j], (i.Child as RowConstant).Fields[j]);
                    }
                    else
                    {
                        keepProj.Add(i.Child.exposedColumns[j]);
                    }
                }
                else
                {
                    splitCols.Add(i.Columns[j]);
                    if (isRowConstant)
                    {
                        rc2.AddValue(i.Child.exposedColumns[j], i.Child.exposedDomains[j], (i.Child as RowConstant).Fields[j]);
                    }
                    else
                    {
                        splitProj.Add(i.Child.exposedColumns[j]);
                    }
                }
            }

            Operator q1 = i.Child;
            Operator q2 = q1.Clone();
            Project pkeep = new Project(q1, keepProj);
            Project psplit = new Project(q2, splitProj);

            if (splitProj.Count > this.inputKeys.Count)
                c.NotifyChange(c.CloneState(new Insert(t, keepCols, (isRowConstant ? rc1 as Operator : pkeep as Operator))),
                    c.CloneState(new Insert(n, splitCols, (isRowConstant ? rc2 as Operator : psplit as Operator))));
            else
                c.NotifyChange(c.CloneState(new Insert(t, keepCols, (isRowConstant ? rc1 as Operator : pkeep as Operator))));

        }*/

        internal override void Expand(Insert i, Command c)
        {
            // If the table doesn't match, punt
            if (i.Table != t)
                return;

            List<string> keepCols = new List<string>();
            List<string> splitCols = new List<string>();

            // Process the child
            i.Child.Visit(this, c);

            var oldRc = i.Child as RowConstant;
            if (oldRc != null)
            {
                RowConstant rc1 = new RowConstant();
                RowConstant rc2 = new RowConstant();

                for (int j = 0; j < i.Columns.Count; j++)
                {
                    if (inputKeys.Contains(i.Columns[j]))
                    {
                        keepCols.Add(i.Columns[j]);
                        splitCols.Add(i.Columns[j]);
                        rc1.AddValue(oldRc.exposedColumns[j],
oldRc.exposedDomains[j], oldRc.Fields[j]);
                        rc2.AddValue(oldRc.exposedColumns[j],
oldRc.exposedDomains[j], oldRc.Fields[j]);
                    }
                    else if (columns.Contains(i.Columns[j]))
                    {
                        keepCols.Add(i.Columns[j]);
                        rc1.AddValue(oldRc.exposedColumns[j],
oldRc.exposedDomains[j], oldRc.Fields[j]);
                    }
                    else
                    {
                        splitCols.Add(i.Columns[j]);
                        rc2.AddValue(oldRc.exposedColumns[j],
oldRc.exposedDomains[j], oldRc.Fields[j]);
                    }
                }

                if (rc2.exposedColumns.Count > this.inputKeys.Count)
                    c.NotifyChange(c.CloneState(new Insert(t, keepCols,
rc1)),
                        c.CloneState(new Insert(n, splitCols, rc2)));
                else
                    c.NotifyChange(c.CloneState(new Insert(t, keepCols,
rc1)));
            }

            else
            {
                List<string> keepProj = new List<string>();
                List<string> splitProj = new List<string>();

                for (int j = 0; j < i.Columns.Count; j++)
                {
                    if (inputKeys.Contains(i.Columns[j]))
                    {
                        keepCols.Add(i.Columns[j]);
                        splitCols.Add(i.Columns[j]);
                        keepProj.Add(i.Child.exposedColumns[j]);
                        splitProj.Add(i.Child.exposedColumns[j]);
                    }
                    else if (columns.Contains(i.Columns[j]))
                    {
                        keepCols.Add(i.Columns[j]);
                        keepProj.Add(i.Child.exposedColumns[j]);
                    }
                    else
                    {
                        splitCols.Add(i.Columns[j]);
                        splitProj.Add(i.Child.exposedColumns[j]);
                    }
                }

                Operator q1 = i.Child;
                Operator q2 = q1.Clone();
                Project pkeep = new Project(q1, keepProj);
                Project psplit = new Project(q2, splitProj);

                if (splitProj.Count > this.inputKeys.Count)
                    c.NotifyChange(c.CloneState(new Insert(t, keepCols,
pkeep)),
                        c.CloneState(new Insert(n, splitCols, psplit)));
                else
                    c.NotifyChange(c.CloneState(new Insert(t, keepCols,
pkeep)));
            }
        }

        internal override void Expand(TableRef tr, Command c)
        {
            if (tr.Table != this.t)
                return;
            List<string> oldColumns = tr.exposedColumns;

            // We have found a table reference that we need to split into a left-outer join
            TableRef left = new TableRef(tr.Table, tr.Alias);
            string newAlias = c.nextTableAlias(this.n);
            TableRef right = new TableRef(this.n, newAlias);

            // Add the column references to the left and right
            for (int i = 0; i < tr.Columns.Count; i++)
            {
                string working = tr.Columns[i];
                if (this.inputKeys.Contains(working))
                {
                    // Add to both sides
                    left.AddColumn(working, tr.exposedColumns[i], tr.exposedDomains[i]);
                    right.AddColumn(working, tr.exposedColumns[i], tr.exposedDomains[i]);
                }
                else if (this.columns.Contains(working))
                {
                    // Add to left side
                    left.AddColumn(working, tr.exposedColumns[i], tr.exposedDomains[i]);
                }
                else
                {
                    // Add to right side
                    right.AddColumn(working, tr.exposedColumns[i], tr.exposedDomains[i]);
                }
            }

            // Create the join and join condition
            Join join = new Join(left, right, JoinType.LEFT);
            foreach (string s in this.inputKeys)
                join.AddCondition(tr.Alias + "." + s, Condition.NewCondition(ConditionType.EQ, new ColumnReference(newAlias + "." + s)));

            // Create a project operator to align the columns
            Project p = new Project(join, oldColumns);

            // Swap out the table reference for this unfolded version
            if (tr.parent != null)
                tr.parent.NotifyChange(tr, p);
        }

        internal override void Expand(Update u, Command c)
        {
            // If the table doesn't match, punt
            if (u.Table != t)
                return;

            // Process the child
            u.Child.Visit(this, c);

            Operator q1 = u.Child;
            Operator q2 = q1.Clone();
            RowConstant rc1 = new RowConstant();
            RowConstant rc2 = new RowConstant();
            bool isRowConstant = q1 is RowConstant;

            List<string> keepCols = new List<string>();
            List<string> splitCols = new List<string>();

            // If child is a full query, copy the query and project only the columns that are needed
            List<string> keepProj = new List<string>();
            List<string> splitProj = new List<string>();
            for (int j = 0; j < u.Columns.Count; j++)
            {
                if (columns.Contains(u.Columns[j]))
                {
                    keepCols.Add(u.Columns[j]);
                    keepProj.Add(u.Child.exposedColumns[j]);
                    if (isRowConstant)
                    {
                        rc1.AddValue(q1.exposedColumns[j], q1.exposedDomains[j], (q1 as RowConstant).Fields[j]);
                    }
                }
                else
                {
                    splitCols.Add(u.Columns[j]);
                    splitProj.Add(u.Child.exposedColumns[j]);
                    if (isRowConstant)
                    {
                        rc2.AddValue(q2.exposedColumns[j], q2.exposedDomains[j], (q2 as RowConstant).Fields[j]);
                    }
                }
            }
            Operator pkeep, psplit;

            if (isRowConstant)
            {
                pkeep = rc1;
                psplit = rc2;
            }
            else
            {
                pkeep = new Project(q1, keepProj);
                psplit = new Project(q2, splitProj);
            }

            string keepalias = c.nextTableAlias(t);
            string splitalias = c.nextTableAlias(n);
            TableRef trkeep = new TableRef(t, keepalias);
            TableRef trsplit = new TableRef(n, splitalias);
            CrossJoin cjkeep = new CrossJoin(pkeep, trsplit);
            CrossJoin cjsplit = new CrossJoin(trkeep, psplit);
            Update ukeep = new Update(t, keepCols, cjkeep);
            Update usplit = new Update(n, splitCols, cjsplit);

            // Propagate the update conditions to the new commands
            for (int i = 0; i < u.CondColumns.Count; i++)
            {
                ukeep.AddCondition(u.CondColumns[i], u.Conditions[i]);
                usplit.AddCondition(u.CondColumns[i], u.Conditions[i]);
            }

            // Create the join statement, just in case
            foreach (string key in this.inputKeys)
            {
                ukeep.AddJoin(key, Condition.NewCondition(ConditionType.EQ, new ColumnReference(splitalias + "." + key)));
                usplit.AddJoin(key, Condition.NewCondition(ConditionType.EQ, new ColumnReference(keepalias + "." + key)));
            }

            if (splitProj.Count > 0)
            {
                // Create an insert statement to fill the missing rows, if necessary
                Command cNew = new Command();
                string keepAlias = cNew.nextTableAlias(t);
                string splitAlias = cNew.nextTableAlias(n);
                Operator keepOp = new TableRef(t, keepAlias, this.inputKeys, this.inputKeys, this.inputKeysDomains);
                Operator splitOp = new TableRef(n, splitAlias, this.inputKeys, this.inputKeys, this.inputKeysDomains);

                for (int iPos = 0; iPos < u.CondColumns.Count; iPos++)
                {
                    keepOp = new Filter(keepOp, new string[] { keepAlias + "." + u.CondColumns[iPos] },
                        new Condition[] { u.Conditions[iPos] });
                    splitOp = new Filter(splitOp, new string[] { splitAlias + "." + u.CondColumns[iPos] },
                        new Condition[] { u.Conditions[iPos] });
                }

                Except ex = new Except(keepOp, splitOp, keepAlias);
                Insert ins = new Insert(n, this.inputKeys, ex);
                cNew.root = ins;

                c.NotifyChange(cNew, c.CloneState(usplit), c.CloneState(ukeep));
            }
            else
                c.NotifyChange(c.CloneState(ukeep));
        }

        public override Transform Inverse()
        {
            VMerge vm = new VMerge(this.t, this.n);
            vm.revOutput = true;
            return vm;
        }

        public override List<XElement> ToXElement()
        {
            XElement x = new XElement("Transform", new XAttribute("type", "VSplit"), new XAttribute("table",t), new XAttribute("new", n));
            for (int i = 0; i < columns.Count; i++)
            {
                bool startsWithNumber = false;
                for (int j = 0; j < 10; j++)
                {
                    if (columns[i].StartsWith(j.ToString()))
                        startsWithNumber = true;
                }

                x.Add(new XElement(columns[i]));
            }
            List<XElement> xList = new List<XElement>();
            if (!(this.Child is SqlProvider))
                xList = ((Transform)this.Child).ToXElement();
            xList.Add(x);
            return xList;
        }

        new public static VSplit FromXElement(XElement x)
        {
            string table = x.Attribute("table").Value;
            string newTable = x.Attribute("new").Value;
            List<string> keepCols = new List<string>();
            foreach (var col in x.Elements())
                keepCols.Add(col.Name.LocalName);
            if (keepCols.Count < 1)
                throw new Exception("While loading VSplit from XML: no keep columns were found");
            if (!(table == null) && !(newTable == null))
                return new VSplit(table, newTable, keepCols);
            else
                throw new Exception("While loading VSplit from XML: table or new names were null");
        }

    }
}
